/*
 * The MIT License
 *
 * Copyright 2014 David Rodenas Pico.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package edu.uoc.sd.fr14.model;

import java.io.Serializable;
import java.util.Map;
import java.util.TreeMap;

/**
 * This class is a class that helps to track last seen/received and applied nseq from other servers.
 * <p>
 * 
 * You should use this to recover parts of the logs and their operations
 * that you might have missing. 
 * You need it to be sure that:
 * - you will never execute twice the same operation (which in fact is not too relevant),
 *   if the nseq is lower than the last seen for that server that means that this is an old
 *   message so you can discard it 
 * - you will never miss data, if you receive a nseq larger that the last seen and applied
 *   operation from that server, you know for sure that you have loose some messages
 *   so you can request/recover them from the last nseq that you have applied
 * - you will apply only what you need, if nseq matches means that what you have been
 *   received is exactly what you have been waiting for, so you can apply to your
 *   own state all received operations (do not save it to your log) and then
 *   annotate/advance the nseq track in order to save that you have seen others sublogs.
 * <p> 
 * 
 * Warning: this class is not thread safe, but if you use it from 
 * one single server instance this is not a problem.
 * 
 * @author drodenas
 */
public final class NseqTracker implements Serializable {
	private static final long serialVersionUID = 555875566071056772L;
	protected NseqTracker() { context = null; }
	
	private static final Logger log = Logger.get("ModelNseqTracker");
    private final Map<ServerId,Nseq> nseqs = new TreeMap<>();

    private final ServerId context; // for logger
            
    NseqTracker(ServerId serverId) {
        this.context = serverId;
    }
    
    /**
     * Advances the last seen/applied nseq for the given server.
     * <p>
     * 
     * This method uses the sublog to know how many positions it has to advance.
     * It suposes that you know for sure that you can do that beceause the sublog 
     * corresponding nseq tracker is exactly what you expect.
     * <p>
     * 
     * You should call it each time that you receive an apply message 
     * only if you can accept this. You should use {@link #isAccepted(Nseq, ServerId)}
     * before. You should also apply {@link SubLog#apply(Company)} 
     * sublog to your application state.
     * 
     * 
     * @param subLog the sublog that its applied satisfactorily because nseq was accepted
     * @param from the server from which comes the sublog
     * @see #isAccepted(Nseq, ServerId)
     */
    public void advance(SubLog subLog, ServerId from) {
        if (subLog == null) throw new NullPointerException("Illegal argument: subLog cannot be null");
        if (from == null) throw new NullPointerException("Illegal argument: from cannot be null");
        log.debug(context, "advance("+subLog+","+from+") nseq:"+getCurrent(from)+"->"+getCurrent(from).next(subLog.size()));
        
        Nseq nseq = getCurrent(from);        
        nseq = nseq.next(subLog.size());
        nseqs.put(from, nseq);
    }
    
    /**
     * It recovers the expected nseq for a given server.
     * <p>
     * 
     * When you need to send recover messages, and you need
     * to tell them which was the last operation confirmed
     * in their log was applied to your state, this method
     * provides the corresponding nseq.
     * <p>
     * 
     * You should call this before send recover to each server
     * to know what to tell them which nseq you need to recover
     * for latter apply. 
     * 
     * @param from the server in which you want to know the last seen/applied nseq
     * @return the nseq corresponding to the last seen/applied operation from the from server commited in from log
     */
    public Nseq getCurrent(ServerId from) {
        if (from == null) throw new NullPointerException("Illegal argument: from cannot be null");
        
        Nseq nseq = nseqs.get(from);
        if (nseq == null) {
            nseq = Nseq.ZERO;
        }
        return nseq;
    }
    
    /**
     * Checks if a given nseq can be received.
     * <p>
     * 
     * You should use it when you receive an apply message in 
     * order to know if you can apply it really. 
     * It will match the last seen/applied expected nseq with
     * the one received from the server from and will return
     * if it matches (if it is expected). 
     * 
     * @param nseq the nseq that you want to check
     * @param from the server that corresponds to the log position of that nseq
     * @return true if you can apply operations of nseq from from, or false if you shuld not (you might ask for recover if not)
     */
    public boolean isAccepted(Nseq nseq, ServerId from) {
        if (nseq == null) throw new NullPointerException("Illegal argument: nseq cannot be null");
        if (from == null) throw new NullPointerException("Illegal argument: from cannot be null");
        
        Nseq current = getCurrent(from);        
        boolean equals = current.equals(nseq);
        return equals;
    }

    @Override
    public String toString() {
        return "Trk="+nseqs;
    }
    
}
